﻿/*
  This file is part of SPARK: SharePoint Application Resource Kit.
  The project is distributed via CodePlex: http://www.codeplex.com/spark/
  Copyright (C) 2003-2010 by Thomas Carpe. http://www.ThomasCarpe.com/

  SPARK is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  SPARK is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with SPARK.  If not, see <http://www.gnu.org/licenses/>.
*/

namespace Behemoth.SharePoint {

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    using System.Text;

    using Microsoft.SharePoint;
  using Microsoft.SharePoint.Administration;
  using Behemoth.SharePoint.Logging;

    public static class SPFieldExtensions {

        #region TryGetField and Supporting Methods

        private const string NO_FIELD_EXCEPTION_MESSAGE = "Value does not fall within the expected range.";

        /// <summary>
        /// Attempts to to get a field by its display name, internal name, or id.
        /// Logs any exceptions, if the field is not found, but returns quietly.
        /// Internal name will take precedence over display name if it is found.
        /// </summary>
        /// <param name="internalNameOrTitle">Name or InternalName of the desired field</param>
        /// <param name="fieldId">Id of the desired field</param>
        /// <param name="fields">Collection of fields to search</param>
        /// <param name="field">An SPField with a matching name or null if not found</param>
        /// <returns>True if successful, false if not</returns>
        public static bool TryGetField(this SPFieldCollection fields, string internalNameOrTitle, out SPField field) {
            field = null;
            try {
                field = fields.GetFieldEx(internalNameOrTitle);
            } catch (ArgumentException ex) {
                if (IsFieldNotFoundException(ex))
                    return false;
                throw ex;
            }
            return true;
        }
        /// <param name="fieldId">Id of the desired field</param>
        /// <param name="fields">Collection of fields to search</param>
        /// <param name="field">An SPField with a matching name or null if not found</param>
        /// <returns>True if successful, false if not</returns>
        public static bool TryGetField(this SPFieldCollection fields, Guid fieldId, out SPField field) {
            field = null;
            try {
                field = fields.GetFieldEx(fieldId);
            } catch (ArgumentException ex) {
                if (IsFieldNotFoundException(ex))
                    return false;
                throw ex;
            }
            return true;
        }

        /// <summary>
        /// Attempts to to get a field by its display name, internal name, or id.
        /// Throws a detailed (and helpful) exception if the field is not found.
        /// Internal name will take precedence over display name if it is found.
        /// </summary>
        /// <param name="internalNameOrTitle">Name or InternalName of the desired field</param>
        /// <param name="fieldId">Id of the desired field</param>
        /// <param name="fields">Collection of fields to search</param>
        /// <returns>An SPField with a matching name or null if not found</returns>
        public static SPField GetFieldEx(this SPFieldCollection fields, string internalNameOrTitle) {
            bool throwException = true;
            SPField field = null;
            Exception caughtEx = null;
            bool canHasField = fields.ContainsField(internalNameOrTitle);
            if (canHasField) {
                try {
                    field = fields.GetFieldByInternalName(internalNameOrTitle);
                } catch (ArgumentException ex) {
                    if (!IsFieldNotFoundException(ex))
                        throw ex;
                    caughtEx = ex;
                }
                if (field != null)
                    return field;
                try {
                    field = fields.GetField(internalNameOrTitle);
                } catch (ArgumentException ex) {
                    if (!IsFieldNotFoundException(ex))
                        throw ex;
                    caughtEx = ex;
                }
            }
            if (field == null && throwException) {
                Exception ex = BuildTryAndGetFieldException(fields, caughtEx, "either the InternalName or Title", internalNameOrTitle);
                BehemothLoggingService.Default.Write(ex.Message, TraceSeverity.Verbose, "Behemoth", "SPFieldExtensions");
                BehemothLoggingService.Default.Write(ex);
                throw ex;
            }
            return field;
        }

        /// <param name="fieldId">Id of the desired field</param>
        /// <param name="fields">Collection of fields to search</param>
        /// <returns>An SPField with a matching name or null if not found</returns>
        public static SPField GetFieldEx(this SPFieldCollection fields, Guid fieldId) {
            bool throwException = true;
            SPField field = null;
            Exception caughtEx = null;
            try {
                field = fields[fieldId];
            } catch (ArgumentException ex) {
                if (!IsFieldNotFoundException(ex))
                    throw ex;
                caughtEx = ex;
            }
            if (field == null && throwException)
                throw BuildTryAndGetFieldException(fields, caughtEx, "Id", fieldId);
            return field;
        }

        private static bool IsFieldNotFoundException(Exception ex) {
            if (ex.GetType() != typeof(ArgumentException))
                return false;
            ArgumentException ax = ex as ArgumentException;
            if (ax.ParamName.Equals(GETFIELDEX_ARGNAME, StringComparison.Ordinal))
                return true;
            // TODO possible to use ex.ParamName, so that'd be more efficient
            return (string.Equals(NO_FIELD_EXCEPTION_MESSAGE, ex.Message, StringComparison.Ordinal));
        }

        private static Exception BuildTryAndGetFieldException(SPFieldCollection fields, Exception ex, string fieldLabel, object fieldNameOrId) {
            string moreDiag = string.Empty;
            if (fields.List != null)
                moreDiag = string.Format("List='{0}' List.ParentWebUrl='{1}'", fields.List.Title, fields.List.ParentWebUrl);
            if (fields.Web != null)
                moreDiag = string.Format("Web.Url='{0}'", fields.Web.Url);
            string msg = string.Format("The field collection does not contain a field with {0} equal to '{1}'. {2}", fieldLabel, fieldNameOrId, moreDiag);
            if (ex == null)
                return new ArgumentException(msg, GETFIELDEX_ARGNAME);
            else
                return new ArgumentException(msg, GETFIELDEX_ARGNAME, ex);
        }

        private const string GETFIELDEX_ARGNAME = "fieldNameOrId";

        #endregion

        // TODO test if this block of code is even needed in SP2010
        #region Reflection Based Field Proeprty Access

        /// <summary>
        /// Uses reflection to access the internal methods of SPField class that can be used to set 
        /// properties, as they are accessible by CAML e.g. &gt;Property Select="PropertyName" /&lt;.
        /// Add calls to this method to your overridden Update() method to allow you Custom Properties
        /// to be read by CAML as regular Properties (a.k.a. Field Attributes).
        /// </summary>
        /// <param name="fieldName">SPField object to which proeprty will be set.</param>
        /// <param name="name">Name of the target property/attribute</param>
        /// <param name="value">Value to be set</param>
        /// <returns>Generally, returns the valueyou passed in on the 'value' parameter</returns>
        public static bool? SetFieldProperty(this SPField field, string name, bool? value) {
            MethodInfo mi = field.GetType().GetMethod("SetFieldAttributeTriValue", BindingFlags.Instance | BindingFlags.NonPublic);
            object[] paramArray = new object[] { name, value };
            object result = mi.Invoke(field, paramArray);
            return (bool?)result;
        }

        public static string SetFieldProperty(this SPField field, string name, string value) {
            MethodInfo mi = field.GetType().GetMethod("SetFieldAttributeValue", BindingFlags.Instance | BindingFlags.NonPublic);
            object[] paramArray = new object[] { name, value };
            object result = mi.Invoke(field, paramArray);
            return (string)result;
        }

        public static bool SetFieldProperty(this SPField field, string name, bool value) {
            MethodInfo mi = field.GetType().GetMethod("SetFieldBoolValue", BindingFlags.Instance | BindingFlags.NonPublic);
            object[] paramArray = new object[] { name, value };
            object result = mi.Invoke(field, paramArray);
            return (bool)result;
        }

        public static int SetFieldProperty(this SPField field, string name, int value) {
            MethodInfo mi = field.GetType().GetMethod("SetFieldIntValue", BindingFlags.Instance | BindingFlags.NonPublic);
            object[] paramArray = new object[] { name, value };
            object result = mi.Invoke(field, paramArray);
            return (int)result;
        }

        #endregion

        public static bool FieldExists(this SPFieldCollection fields, string internalNameOrTitle) {
            return fields.ContainsField(internalNameOrTitle);
        }
        public static bool FieldExists(this SPFieldCollection fields, Guid id) {
            SPField field = null;
            try {
                field = fields[id];
            } catch { /* (ArgumentException ex)
            if (!string.Equals(NO_FIELD_EXCEPTION_MESSAGE, ex.Message, StringComparison.Ordinal))
                throw ex; */
            }
            return (field != null);
        }

        /// <summary>
        /// Because for some reason when you need to set the value of a list item
        /// your *only* choices are its Title (yuck!) or the fieldName Id... &lt;sigh /&gt;
        /// </summary>
        /// <param name="internalNameOrTitle">Name or InternalName of the desired field</param>
        /// <param name="fields">Collection of fields to search</param>
        /// <returns>The field Id, or Guid.Empty if the name does not correspond to a valid field</returns>
        public static Guid GetFieldId(this SPFieldCollection fields, string internalNameOrTitle) {
            SPField field = null;
            bool success = fields.TryGetField(internalNameOrTitle, out field);
            if (!success)
                return Guid.Empty;
            return field.Id;
        }

        /// <summary>
        /// This function takes a list of field (display or internal) names
        /// or field ID guids, and creates a generic list of SPField objects.
        /// The function looks for the field first in the web and then in the 
        /// web.Site.RootWeb. Only fields that are found are added to the list.
        /// </summary>
        /// <param name="web">The web to search for the fields within.</param>
        /// <param name="fields">A list of fieldName internal names, titles, or a list of Ids.</param>
        /// <returns>A generic List of SPField objects that match the names or Ids requested</returns>
        public static List<SPField> MakeFieldsList(this SPWeb web, List<string> fields) {
            List<SPField> fieldsList = new List<SPField>();
            foreach (string fieldName in fields) {
                SPField field = null;
                bool success = web.Fields.TryGetField(fieldName, out field);
                if (!success || field != null)
                    success = web.Site.RootWeb.Fields.TryGetField(fieldName, out field);
                if (!success || field == null) {
                    BehemothLoggingService.Default.Write(string.Format(
                        "Attempt to add field '{0}' to field list for web '{1}'. Most likely that field does not exist in web or site root web.",
                        fieldName, web.Url
                    ), TraceSeverity.Verbose, "Behemoth", "SPFieldExtensions");
                    continue;
                }
                fieldsList.Add(field);
            }
            return fieldsList;
        }
        /// <param name="web">The web to search for the fields within.</param>
        /// <param name="fields">A list of fieldName internal names, titles, or a list of Ids.</param>
        /// <returns>A generic List of SPField objects that match the names or Ids requested</returns>
        public static List<SPField> MakeFieldsList(this SPWeb web, List<Guid> fields) {
            List<SPField> fieldsList = new List<SPField>();
            foreach (Guid fieldId in fields) {
                SPField field = null;
                bool success = web.Fields.TryGetField(fieldId, out field);
                if (!success || field != null)
                    success = web.Site.RootWeb.Fields.TryGetField(fieldId, out field);
                if (!success || field == null) {
                    BehemothLoggingService.Default.Write(string.Format(
                        "Attempt to add field '{0}' to field list for web '{1}'. Most likely that field does not exist in web or site root web.",
                        fieldId, web.Url
                    ), TraceSeverity.Verbose, "Behemoth", "SPFieldExtensions");
                    continue;
                }
                fieldsList.Add(field);
            }
            return fieldsList;
        }

        public static void SetDisplayOnlyFieldProperties(this SPField field, string newFormDisplayMode, string editFormDisplayMode, bool doUpdate) {
            if (string.IsNullOrEmpty(newFormDisplayMode))
                newFormDisplayMode = "Display Only";
            if (string.IsNullOrEmpty(editFormDisplayMode))
                editFormDisplayMode = "Display Only";
            field.SetCustomProperty("NewFormDisplayMode", newFormDisplayMode);
            field.SetCustomProperty("EditFormDisplayMode", editFormDisplayMode);
            if (doUpdate)
                field.Update();
        }

    }  // class

} // namespace
